package com.onadake.utils;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 * Contains static method which simplifies the manipulation with domain objects.
 * 
 * @author radek.hecl
 *
 */
public class DomainUtils {

    /**
     * Global implementation of unique code generator.
     * This guarantees all the generated codes are unique since start of the application.
     * This implementation is thread safe.
     */
    private static UniqueCodeGenerator uniqueCodeGenerator = new UniqueCodeGenerator() {

        /**
         * Time part which was used for the last time.
         */
        private String lastTimePart = null;
        
        /**
         * Internal object for locking.
         */
        private Lock lock = new ReentrantLock();

        @Override
        public String generateUniqueAlphanumeric(String prefix) {
            lock.lock();
            try {
                String tm = StringUtils.leftPad(Long.toString(Dates.now().getTime(), Character.MAX_RADIX).toUpperCase(), 11, '0');
                while (tm.equals(lastTimePart)) {
                    tm = StringUtils.leftPad(Long.toString(Dates.now().getTime(), Character.MAX_RADIX).toUpperCase(), 11, '0');
                }
                lastTimePart = tm;
                return prefix + tm + RandomStringUtils.randomAlphanumeric(2).toUpperCase();
            } finally {
                lock.unlock();
            }
        }

        @Override
        public Set<String> generateUniqueAlphanumericBulk(String prefix, int number) {
            lock.lock();
            try {
                Set<String> res = new HashSet<>();
                while (res.size() != number) {
                    String tm = StringUtils.leftPad(Long.toString(Dates.now().getTime(), Character.MAX_RADIX).toUpperCase(), 11, '0');
                    while (tm.equals(lastTimePart)) {
                        tm = StringUtils.leftPad(Long.toString(Dates.now().getTime(), Character.MAX_RADIX).toUpperCase(), 11, '0');
                    }
                    lastTimePart = tm;
                    int currentSize = res.size();
                    for (int i = 0; i < number - currentSize; ++i) {
                        res.add(prefix + tm + RandomStringUtils.randomAlphanumeric(2).toUpperCase());
                    }
                }
                return res;
            } finally {
                lock.unlock();
            }
        }

    };

    /**
     * Null safe copy date function.
     * 
     * @param source source date, can be null
     * @return copy of the source date or null if source is null 
     */
    public static Date copyDate(Date source) {
        if (source == null) {
            return null;
        }
        return new Date(source.getTime());
    }

    /**
     * Null safe function for creating a soft copy of the list.
     * It the source is null, then empty list will be returned.
     * 
     * @param source source object, can be any collection
     * @return soft copy of the source object
     */
    public static <T> List<T> softCopyList(Collection<T> source) {
        if (source == null) {
            source = Collections.emptyList();
        }
        return new ArrayList<T>(source);
    }

    /**
     * Null safe function for creating a soft copy of the set.
     * If the source is null, then empty set will be returned.
     * 
     * @param source source object, can be any collection
     * @return soft copy of the source object
     */
    public static <T> Set<T> softCopySet(Collection<T> source) {
        if (source == null) {
            source = Collections.emptySet();
        }
        return new HashSet<T>(source);
    }

    /**
     * Null safe function for creating a soft copy of the map.
     * If the source is null, then empty map will be returned.
     * 
     * @param source source object
     * @return soft copy of the source object
     */
    public static <K, V> Map<K, V> softCopyMap(Map<K, V> source) {
        if (source == null) {
            source = Collections.emptyMap();
        }
        return new HashMap<K, V>(source);
    }

    /**
     * Make a soft copy of the source map, but without the keys which match the specified regular expression.
     * For example if the given map is { "hello" : 1, "world" : 2} and regexp is "hello", then
     * result copy will be map { "hello" : 1 }.
     * 
     * @param source source map, must be without null keys
     * @param regexp regular expression to filter out the keys from the copy of the map
     * @return soft copy of the source map without the keys which are matching the expression
     */
    public static <V> Map<String, V> softCopyMapWithoutMatchedKeys(Map<String, V> source, String regexp) {
        Map<String, V> res = new HashMap<>();
        for (String key : source.keySet()) {
            if (!key.matches(regexp)) {
                res.put(key, source.get(key));
            }
        }
        return res;
    }

    /**
     * Merges several maps into one.
     * If there is no map to merge, then returns empty map.
     * If there is at least one map, then takes elements form the first, then apply from the second
     * and so on.
     * Examples (in JSON notation):
     * <ul>
     *   <li>{"a" : 1} + {"b" : 2} -> {"a" : 1, "b" : 2}</li>
     *   <li>{"a" : 1} + {"a" : 2} -> {"a" : 2}</li>
     *   <li>{"a" : 1} + {"b" : 2} + {"a" : "hello"} -> {"a" : "hello", "b" : 2}</li>
     * </ul>
     * 
     * @param maps maps to merge 
     * @return merged map
     */
    @SafeVarargs
    public static <K, V> Map<K, V> mergeMaps(Map<K, V>... maps) {
        Map<K, V> res = new HashMap<>();
        for (int i = 0; i < maps.length; ++i) {
            res.putAll(maps[i]);
        }
        return res;
    }

    /**
     * Creates map where prefix is added to the each key. 
     * Examples:
     * <ul>
     *   <li>"hello.", {"world" : 1, "baby" : 2} -> {"hello.world" : 1, "hello.baby" : 2}</li>
     *   <li>"$", {"world" : 1, "baby" : 2} -> {"$world" : 1, "$baby" : 2}</li>
     * </ul>
     * 
     * @param prefix prefix which will be used, cannot be null
     * @param source source map, cannot be null or have null as a key
     * @return map with all keys being prefixed
     */
    public static <T> Map<String, T> createMapPrefixOnKeys(String prefix, Map<String, T> source) {
        ValidationUtils.guardNotNull(prefix, "prefix cannot be null");
        ValidationUtils.guardNotNull(source, "source cannot be null");
        ValidationUtils.guardNotNullCollection(source.keySet(), "source.keySet() cannot have null element");
        Map<String, T> res = new HashMap<>();
        for (String key : source.keySet()) {
            res.put(prefix + key, source.get(key));
        }
        return res;
    }
    
    /**
     * Returns the copy of the object. This is not the deep copy.
     * Should provide the balance between performance and security.
     * If the source is null, then null will be returned.
     * If the source is date, then the copy will be returned.
     * If the source is list, set or map, then soft copy will be returned.
     * If the source is any other type, then source object will be returned.
     *  
     * @param source source object
     * @return copy of the given object
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static Object copyObject(Object source) {
        if (source instanceof Date) {
            return copyDate((Date) source);
        }
        if (source instanceof List) {
            return softCopyList((List) source);
        }
        if (source instanceof Set) {
            return softCopySet((Set) source);
        }
        if (source instanceof Map) {
            return softCopyMap((Map) source);
        }
        return source;
    }

    /**
     * Wrapper function around java.util.Arrays.asList(T... a) to create a set of objects.
     * 
     * @return created set
     */
    @SafeVarargs
    public static <T> Set<T> createSet(T... a) {
        return new HashSet<T>(Arrays.asList(a));
    }

    /**
     * Returns the set which contains all the numbers from the specified interval.
     * Borders are included.
     * Examples:
     * <ul>
     *   <li>min = 1, max = 3 -> [1, 2, 3]</li>
     *   <li>min = 1, max = 1 -> [1]</li>
     *   <li>min = 1, max = 0 -> []</li>
     * </ul>
     * 
     * @param min minimum (included)
     * @param max maximum (included)
     * @return created interval set
     */
    public static Set<Long> createLongIntervalSet(long min, long max) {
        Set<Long> res = new HashSet<>();
        for (long i = min; i <= max; ++i) {
            res.add(i);
        }
        return res;
    }

    /**
     * Takes the collection and splits it into the several lists.
     * Every of these lists has maximum number of elements which is less or equal to the max parameters.
     * If source collection has some duplicated elements, then all of them stays there.
     * If source collection is null or empty, then list with no sub lists will be returned.
     * The ordering within the split lists is the same as within the source collection.
     * 
     * @param source source collection
     * @param max maximum allowed number of elements per one list, must be positive number
     * @return source collection split into lists
     */
    public static <T> List<List<T>> splitIntoLists(Collection<T> source, int max) {
        ValidationUtils.guardPositiveInt(max, "max must be positive number");
        List<T> sourceCopy = softCopyList(source);
        if (sourceCopy.isEmpty()) {
            return Collections.emptyList();
        }
        List<List<T>> res = new ArrayList<>();
        List<T> part = new ArrayList<>();
        for (T obj : sourceCopy) {
            part.add(obj);
            if (part.size() == max) {
                res.add(part);
                part = new ArrayList<>();
            }
        }
        if (!part.isEmpty()) {
            res.add(part);
        }
        return res;
    }

    /**
     * Takes the collection and splits it into the several sets.
     * Every of these sets has maximum number of elements which is less or equal to the max parameters.
     * If source collection has some duplicated elements, then all of them are merged.
     * If source collection is null or empty, then list with no sub lists will be returned.
     * The ordering within the split sets is not defined.
     * 
     * @param source source collection
     * @param max maximum allowed number of elements per one list, must be positive number
     * @return source collection split into lists
     */
    public static <T> List<Set<T>> splitIntoSets(Collection<T> source, int max) {
        ValidationUtils.guardPositiveInt(max, "max must be positive number");
        Set<T> sourceCopy = softCopySet(source);
        if (sourceCopy.isEmpty()) {
            return Collections.emptyList();
        }
        List<Set<T>> res = new ArrayList<>();
        Set<T> part = new HashSet<>();
        for (T obj : sourceCopy) {
            part.add(obj);
            if (part.size() == max) {
                res.add(part);
                part = new HashSet<>();
            }
        }
        if (!part.isEmpty()) {
            res.add(part);
        }
        return res;
    }

    /**
     * Creates the document from a given XML file.
     * 
     * @param file file to parse
     * @return created DOM document
     */
    public static Document createDocumentFromXml(File file) {
        InputStream is = null;
        try {
            is = new FileInputStream(file);
            DocumentBuilder docBuilder = DocumentBuilderFactory.
                    newInstance().
                    newDocumentBuilder();
            Document doc = docBuilder.parse(is);
            is.close();
            return doc;
        } catch (ParserConfigurationException | SAXException | IOException e) {
            throw new RuntimeException(e);
        } finally {
            IOUtils.closeQuietly(is);
        }
    }

    /**
     * Returns object property by the specified enumeration name.
     * First searches for getter which starting with "get".
     * If this getter is not presented, then search for the one which starting with "is".
     * Examples of getters for a specified enumeration:
     * <ul>
     *   <li>ID => getId(), isId()</li>
     *   <li>EVENT_TIMESTAMP => getEventTimestamp(), isEventTimestamp()</li>
     * </ul>
     * If getter is presented, then returns the value.
     * Note: This method uses reflection, so speed is not great.
     *  
     * @param obj object from which property should be obtained
     * @param enm enumeration which will be used to find the property
     * @return value of the property
     */
    public static Object getPropertyByEnum(Object obj, Enum<?> enm) {
        ValidationUtils.guardNotNull(obj, "obj cannot be null");
        ValidationUtils.guardNotNull(enm, "enm cannot be null");
        String name = enm.name().toLowerCase();
        String getterBase = "";
        boolean capital = true;
        for (int i = 0; i < name.length(); ++i) {
            char c = name.charAt(i);
            if (c == '_') {
                capital = true;
            }
            else if (capital) {
                getterBase += String.valueOf(c).toUpperCase();
                capital = false;
            }
            else {
                getterBase += String.valueOf(c);
            }
        }
        Method method = null;
        Class<?> clazz = obj.getClass();
        try {
            method = clazz.getMethod("get" + getterBase);
        } catch (NoSuchMethodException e) {
            try {
                method = clazz.getMethod("is" + getterBase);
            } catch (NoSuchMethodException e1) {
                throw new IllegalArgumentException("getter doesn't exists: Class = " + clazz + "; Enum = " + enm);
            } catch (SecurityException e1) {
                throw new RuntimeException(e1);
            }
        } catch (SecurityException e) {
            throw new RuntimeException(e);
        }
        try {
            return method.invoke(obj);
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Null safe equals method. Returns true if both objects are null or equals each other.
     * This method expects equals method to be well defined (reflexivity, symetricity, transitivity) in both objects.
     * 
     * @param obj1 first object
     * @param obj2 second object
     * @return true if both objects are null or if they equals, false otherwise
     */
    public static boolean safeEquals(Object obj1, Object obj2) {
        if (obj1 == null && obj2 == null) {
            return true;
        }
        if (obj1 == null || obj2 == null) {
            return false;
        }
        return obj1.equals(obj2);
    }

    /**
     * Converts property map into the velocity model.
     * This means deep conversion of these property key types is done.
     * <ul>
     *   <li>Properties with key in XX format are converted as they are.</li>
     *   <li>Properties with key in XX.YY format are converted into map XX = {YY}</li>
     *   <li>Properties with key in XX[NUM] format are converted into list</li>
     *   <li>Properties with key in XX[NUM].YY format are converted into list of maps</li>
     * </ul>
     * 
     * @param properties properties to be converted
     * @return velocity model
     */
    public static <T> Map<String, Object> convertPropertyMapIntoVelocityModel(Map<String, T> properties) {
        Map<String, Object> res = new HashMap<>();
        Map<String, Map<String, T>> maps = new HashMap<>();
        Map<String, SortedMap<Integer, Map<String, T>>> arrayMaps = new HashMap<>();
        Map<String, SortedMap<Integer, T>> lists = new HashMap<>();
        for (String key : properties.keySet()) {
            if (key.matches("^[^\\.\\[\\]]+$")) {
                res.put(key, properties.get(key));
            }
            else if (key.matches("^[^\\[\\]]+\\..+$")) {
                String[] parts = key.split("\\.", 2);
                String mapKey = parts[0];
                String subKey = parts[1];
                if (!maps.containsKey(mapKey)) {
                    maps.put(mapKey, new HashMap<String, T>());
                }
                maps.get(mapKey).put(subKey, properties.get(key));
            }
            else if (key.matches("^[^\\.]+\\[[0-9]+\\]\\..+$")) {
                String[] parts = key.split("[\\[\\]]", 3);
                String mapKey = parts[0];
                int index = Integer.valueOf(parts[1]);
                String subKey = parts[2].substring(1);
                if (!arrayMaps.containsKey(mapKey)) {
                    arrayMaps.put(mapKey, new TreeMap<Integer, Map<String, T>>());
                }
                if (!arrayMaps.get(mapKey).containsKey(index)) {
                    arrayMaps.get(mapKey).put(index, new HashMap<String, T>());
                }
                arrayMaps.get(mapKey).get(index).put(subKey, properties.get(key));
            }
            else if (key.matches("^[^\\.]+\\[[0-9]+\\]$")) {
                String[] parts = key.split("[\\[\\]]", 3);
                String mapKey = parts[0];
                int index = Integer.valueOf(parts[1]);
                if (!lists.containsKey(mapKey)) {
                    lists.put(mapKey, new TreeMap<Integer, T>());
                }
                lists.get(mapKey).put(index, properties.get(key));
            }
            else {
                throw new RuntimeException("unsupported property key, please implement me: key = " + key);
            }
        }
        // transform maps and arrays
        for (String key : maps.keySet()) {
            res.put(key, convertPropertyMapIntoVelocityModel(maps.get(key)));
        }
        for (String key : arrayMaps.keySet()) {
            List<Map<String, Object>> list = new ArrayList<>();
            for (int index : arrayMaps.get(key).keySet()) {
                list.add(convertPropertyMapIntoVelocityModel(arrayMaps.get(key).get(index)));
            }
            res.put(key, list);
        }
        for (String key : lists.keySet()) {
            List<T> list = new ArrayList<>();
            for (int index : lists.get(key).keySet()) {
                list.add(lists.get(key).get(index));
            }
            res.put(key, list);
        }
        return res;
    }
    
    /**
     * Returns the unique code generator. Returned implementation is thread safe
     * and all codes generated since the start of application are guaranteed to be unique.
     * 
     * @return instance of unique code generator
     */
    public static UniqueCodeGenerator getUniqueCodeGenerator() {
        return uniqueCodeGenerator;
    }

}
